/******************************************************************************
 *
 * Project:  GIF Driver
 * Purpose:  Implement GDAL GIF Support using libungif code.
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 2001, Frank Warmerdam
 * Copyright (c) 2007-2012, Even Rouault <even dot rouault at spatialys.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/

#include "gifabstractdataset.h"

#include "cpl_string.h"
#include "gdal_frmts.h"
#include "gdal_pam.h"
#include "gifdrivercore.h"

CPL_C_START
#if !(defined(GIFLIB_MAJOR) && GIFLIB_MAJOR >= 5)

// This prototype seems to have been messed up!
GifFileType *EGifOpen(void *userData, OutputFunc writeFunc);

// Define alias compatible with giflib >= 5.0.0
#define GifMakeMapObject MakeMapObject
#define GifFreeMapObject FreeMapObject

#endif  // defined(GIFLIB_MAJOR) && GIFLIB_MAJOR < 5

CPL_C_END

/************************************************************************/
/*                          VSIGIFWriteFunc()                           */
/*                                                                      */
/*      Proxy write function.                                           */
/************************************************************************/

static int VSIGIFWriteFunc(GifFileType *psGFile, const GifByteType *pabyBuffer,
                           int nBytesToWrite)

{
    VSILFILE *fp = static_cast<VSILFILE *>(psGFile->UserData);
    if (VSIFTellL(fp) == 0 && nBytesToWrite >= 6 &&
        memcmp(pabyBuffer, "GIF87a", 6) == 0)
    {
        // This is a hack to write a GIF89a instead of GIF87a (we have to, since
        // we are using graphical extension block).  EGifSpew would write GIF89a
        // when it detects an extension block if we were using it As we don't,
        // we could have used EGifSetGifVersion instead, but the version of
        // libungif in GDAL has a bug: it writes on read-only memory!
        // This is a well-known problem. Just google for "EGifSetGifVersion
        // segfault".
        // Most readers don't even care if it is GIF87a or GIF89a, but it is
        // better to write the right version.

        size_t nRet = VSIFWriteL("GIF89a", 1, 6, fp);
        nRet += VSIFWriteL(reinterpret_cast<const char *>(pabyBuffer) + 6, 1,
                           nBytesToWrite - 6, fp);
        return static_cast<int>(nRet);
    }

    return static_cast<int>(VSIFWriteL(pabyBuffer, 1, nBytesToWrite, fp));
}

/************************************************************************/
/* ==================================================================== */
/*                                  GIFDataset                          */
/* ==================================================================== */
/************************************************************************/

class GIFRasterBand;

class GIFDataset final : public GIFAbstractDataset
{
    friend class GIFRasterBand;

  public:
    GIFDataset();

    static GDALDataset *Open(GDALOpenInfo *);

    static GDALDataset *CreateCopy(const char *pszFilename,
                                   GDALDataset *poSrcDS, int bStrict,
                                   char **papszOptions,
                                   GDALProgressFunc pfnProgress,
                                   void *pProgressData);
};

/************************************************************************/
/* ==================================================================== */
/*                            GIFRasterBand                             */
/* ==================================================================== */
/************************************************************************/

class GIFRasterBand final : public GIFAbstractRasterBand
{
  public:
    GIFRasterBand(GIFDataset *, int, SavedImage *, int);
    CPLErr IReadBlock(int, int, void *) override;
};

/************************************************************************/
/*                           GIFRasterBand()                            */
/************************************************************************/

GIFRasterBand::GIFRasterBand(GIFDataset *poDSIn, int nBandIn,
                             SavedImage *psSavedImage, int nBackground)
    : GIFAbstractRasterBand(poDSIn, nBandIn, psSavedImage, nBackground, FALSE)
{
}

/************************************************************************/
/*                             IReadBlock()                             */
/************************************************************************/

CPLErr GIFRasterBand::IReadBlock(CPL_UNUSED int nBlockXOff, int nBlockYOff,
                                 void *pImage)
{
    CPLAssert(nBlockXOff == 0);

    if (psImage == nullptr)
    {
        memset(pImage, 0, nBlockXSize);
        return CE_None;
    }

    if (panInterlaceMap != nullptr)
        nBlockYOff = panInterlaceMap[nBlockYOff];

    memcpy(pImage, psImage->RasterBits + nBlockYOff * nBlockXSize, nBlockXSize);

    return CE_None;
}

/************************************************************************/
/* ==================================================================== */
/*                             GIFDataset                               */
/* ==================================================================== */
/************************************************************************/

/************************************************************************/
/*                            GIFDataset()                            */
/************************************************************************/

GIFDataset::GIFDataset()
{
}

/************************************************************************/
/*                                Open()                                */
/************************************************************************/

GDALDataset *GIFDataset::Open(GDALOpenInfo *poOpenInfo)

{
    if (!GIFDriverIdentify(poOpenInfo))
        return nullptr;

    if (poOpenInfo->eAccess == GA_Update)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "The GIF driver does not support update access to existing"
                 " files.");
        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Ingest.                                                         */
    /* -------------------------------------------------------------------- */
    VSILFILE *fp = poOpenInfo->fpL;
    poOpenInfo->fpL = nullptr;

    GifFileType *hGifFile =
        GIFAbstractDataset::myDGifOpen(fp, GIFAbstractDataset::ReadFunc);
    if (hGifFile == nullptr)
    {
        VSIFCloseL(fp);
        CPLError(CE_Failure, CPLE_OpenFailed,
                 "DGifOpen() failed for %s.  "
                 "Perhaps the gif file is corrupt?",
                 poOpenInfo->pszFilename);

        return nullptr;
    }

    // The following code enables us to detect GIF datasets eligible
    // for BIGGIF driver even with an unpatched giflib.

    /* -------------------------------------------------------------------- */
    /*      Find the first image record.                                    */
    /* -------------------------------------------------------------------- */
    GifRecordType RecordType = FindFirstImage(hGifFile);
    if (RecordType == IMAGE_DESC_RECORD_TYPE &&
        DGifGetImageDesc(hGifFile) != GIF_ERROR)
    {
        const int width = hGifFile->SavedImages[0].ImageDesc.Width;
        const int height = hGifFile->SavedImages[0].ImageDesc.Height;
        if (static_cast<double>(width) * height > 100000000.0)
        {
            CPLDebug("GIF", "Due to limitations of the GDAL GIF driver we "
                            "deliberately avoid opening large GIF files "
                            "(larger than 100 megapixels).");
            GIFAbstractDataset::myDGifCloseFile(hGifFile);
            // Reset poOpenInfo->fpL since BIGGIF may need it.
            poOpenInfo->fpL = fp;
            VSIFSeekL(fp, 0, SEEK_SET);
            return nullptr;
        }
    }

    GIFAbstractDataset::myDGifCloseFile(hGifFile);

    VSIFSeekL(fp, 0, SEEK_SET);

    hGifFile = GIFAbstractDataset::myDGifOpen(fp, GIFAbstractDataset::ReadFunc);
    if (hGifFile == nullptr)
    {
        VSIFCloseL(fp);
        CPLError(CE_Failure, CPLE_OpenFailed,
                 "DGifOpen() failed for %s.  "
                 "Perhaps the gif file is corrupt?",
                 poOpenInfo->pszFilename);

        return nullptr;
    }

    const int nGifErr = DGifSlurp(hGifFile);

    if (nGifErr != GIF_OK || hGifFile->SavedImages == nullptr)
    {
        VSIFCloseL(fp);
        GIFAbstractDataset::myDGifCloseFile(hGifFile);

        if (nGifErr == D_GIF_ERR_DATA_TOO_BIG)
        {
            CPLDebug("GIF",
                     "DGifSlurp() failed for %s because it was too large.  "
                     "Due to limitations of the GDAL GIF driver we "
                     "deliberately avoid opening large GIF files "
                     "(larger than 100 megapixels).",
                     poOpenInfo->pszFilename);
            return nullptr;
        }

        CPLError(CE_Failure, CPLE_OpenFailed,
                 "DGifSlurp() failed for %s.  "
                 "Perhaps the gif file is corrupt?",
                 poOpenInfo->pszFilename);

        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Create a corresponding GDALDataset.                             */
    /* -------------------------------------------------------------------- */
    GIFDataset *poDS = new GIFDataset();

    poDS->fp = fp;
    poDS->eAccess = GA_ReadOnly;
    poDS->hGifFile = hGifFile;

    /* -------------------------------------------------------------------- */
    /*      Capture some information from the file that is of interest.     */
    /* -------------------------------------------------------------------- */
    poDS->nRasterXSize = hGifFile->SavedImages[0].ImageDesc.Width;
    poDS->nRasterYSize = hGifFile->SavedImages[0].ImageDesc.Height;
    if (!GDALCheckDatasetDimensions(poDS->nRasterXSize, poDS->nRasterYSize))
    {
        delete poDS;
        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Create band information objects.                                */
    /* -------------------------------------------------------------------- */
    for (int iImage = 0; iImage < hGifFile->ImageCount; iImage++)
    {
        SavedImage *psImage = hGifFile->SavedImages + iImage;

        if (psImage->ImageDesc.Width != poDS->nRasterXSize ||
            psImage->ImageDesc.Height != poDS->nRasterYSize)
            continue;

        if (psImage->ImageDesc.ColorMap == nullptr &&
            poDS->hGifFile->SColorMap == nullptr)
        {
            CPLDebug("GIF", "Skipping image without color table");
            continue;
        }
#if defined(GIFLIB_MAJOR) && GIFLIB_MAJOR >= 5
        // Since giflib 5, de-interlacing is done by DGifSlurp().
        psImage->ImageDesc.Interlace = false;
#endif
        poDS->SetBand(poDS->nBands + 1,
                      new GIFRasterBand(poDS, poDS->nBands + 1, psImage,
                                        hGifFile->SBackGroundColor));
    }
    if (poDS->nBands == 0)
    {
        delete poDS;
        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Check for georeferencing.                                       */
    /* -------------------------------------------------------------------- */
    poDS->DetectGeoreferencing(poOpenInfo);

    /* -------------------------------------------------------------------- */
    /*      Initialize any PAM information.                                 */
    /* -------------------------------------------------------------------- */
    poDS->SetDescription(poOpenInfo->pszFilename);
    poDS->TryLoadXML(poOpenInfo->GetSiblingFiles());

    /* -------------------------------------------------------------------- */
    /*      Support overviews.                                              */
    /* -------------------------------------------------------------------- */
    poDS->oOvManager.Initialize(poDS, poOpenInfo->pszFilename,
                                poOpenInfo->GetSiblingFiles());

    return poDS;
}

/************************************************************************/
/*                        GDALPrintGifError()                           */
/************************************************************************/

static void GDALPrintGifError(CPL_UNUSED GifFileType *hGifFile,
                              const char *pszMsg)
{
    // GIFLIB_MAJOR is only defined in libgif >= 4.2.0.
    // libgif 4.2.0 has retired PrintGifError() and added GifErrorString().
#if defined(GIFLIB_MAJOR) && defined(GIFLIB_MINOR) &&                          \
    ((GIFLIB_MAJOR == 4 && GIFLIB_MINOR >= 2) || GIFLIB_MAJOR > 4)
    // Static string actually, hence the const char* cast.

#if GIFLIB_MAJOR >= 5
    const char *pszGIFLIBError = GifErrorString(hGifFile->Error);
#else
    // TODO(schwehr): Can we remove the cast for older libgif?
    const char *pszGIFLIBError = (const char *)GifErrorString();
#endif
    if (pszGIFLIBError == nullptr)
        pszGIFLIBError = "Unknown error";
    CPLError(CE_Failure, CPLE_AppDefined, "%s. GIFLib Error : %s", pszMsg,
             pszGIFLIBError);
#else
    PrintGifError();
    CPLError(CE_Failure, CPLE_AppDefined, "%s", pszMsg);
#endif
}

/************************************************************************/
/*                             CreateCopy()                             */
/************************************************************************/

GDALDataset *GIFDataset::CreateCopy(const char *pszFilename,
                                    GDALDataset *poSrcDS, int bStrict,
                                    char **papszOptions,
                                    GDALProgressFunc pfnProgress,
                                    void *pProgressData)

{
    /* -------------------------------------------------------------------- */
    /*      Check for interlaced option.                                    */
    /* -------------------------------------------------------------------- */
    const bool bInterlace = CPLFetchBool(papszOptions, "INTERLACING", false);

    /* -------------------------------------------------------------------- */
    /*      Some some rudimentary checks                                    */
    /* -------------------------------------------------------------------- */
    const int nBands = poSrcDS->GetRasterCount();
    if (nBands != 1)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "GIF driver only supports one band images.");

        return nullptr;
    }

    const int nXSize = poSrcDS->GetRasterXSize();
    const int nYSize = poSrcDS->GetRasterYSize();
    if (nXSize > 65535 || nYSize > 65535)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "GIF driver only supports datasets up to 65535x65535 size.");

        return nullptr;
    }

    if (poSrcDS->GetRasterBand(1)->GetRasterDataType() != GDT_Byte && bStrict)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "GIF driver doesn't support data type %s. "
                 "Only eight bit bands supported.",
                 GDALGetDataTypeName(
                     poSrcDS->GetRasterBand(1)->GetRasterDataType()));

        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Open the output file.                                           */
    /* -------------------------------------------------------------------- */
    VSILFILE *fp = VSIFOpenL(pszFilename, "wb");
    if (fp == nullptr)
    {
        CPLError(CE_Failure, CPLE_OpenFailed, "Failed to create %s:\n%s",
                 pszFilename, VSIStrerror(errno));
        return nullptr;
    }

#if defined(GIFLIB_MAJOR) && GIFLIB_MAJOR >= 5
    int nError = 0;
    GifFileType *hGifFile = EGifOpen(fp, VSIGIFWriteFunc, &nError);
#else
    GifFileType *hGifFile = EGifOpen(fp, VSIGIFWriteFunc);
#endif
    if (hGifFile == nullptr)
    {
        VSIFCloseL(fp);
        CPLError(CE_Failure, CPLE_OpenFailed,
                 "EGifOpenFilename(%s) failed.  Does file already exist?",
                 pszFilename);

        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Prepare colortable.                                             */
    /* -------------------------------------------------------------------- */
    GDALRasterBand *poBand = poSrcDS->GetRasterBand(1);
    ColorMapObject *psGifCT = nullptr;

    if (poBand->GetColorTable() == nullptr)
    {
        psGifCT = GifMakeMapObject(256, nullptr);
        if (psGifCT == nullptr)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Cannot allocate color table");
            GIFAbstractDataset::myEGifCloseFile(hGifFile);
            VSIFCloseL(fp);
            return nullptr;
        }
        for (int iColor = 0; iColor < 256; iColor++)
        {
            psGifCT->Colors[iColor].Red = static_cast<GifByteType>(iColor);
            psGifCT->Colors[iColor].Green = static_cast<GifByteType>(iColor);
            psGifCT->Colors[iColor].Blue = static_cast<GifByteType>(iColor);
        }
    }
    else
    {
        GDALColorTable *poCT = poBand->GetColorTable();
        int nFullCount = 2;

        while (nFullCount < poCT->GetColorEntryCount())
            nFullCount = nFullCount * 2;

        psGifCT = GifMakeMapObject(nFullCount, nullptr);
        if (psGifCT == nullptr)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Cannot allocate color table");
            GIFAbstractDataset::myEGifCloseFile(hGifFile);
            VSIFCloseL(fp);
            return nullptr;
        }
        int iColor = 0;
        for (; iColor < poCT->GetColorEntryCount(); iColor++)
        {
            GDALColorEntry sEntry;

            poCT->GetColorEntryAsRGB(iColor, &sEntry);
            psGifCT->Colors[iColor].Red = static_cast<GifByteType>(sEntry.c1);
            psGifCT->Colors[iColor].Green = static_cast<GifByteType>(sEntry.c2);
            psGifCT->Colors[iColor].Blue = static_cast<GifByteType>(sEntry.c3);
        }
        for (; iColor < nFullCount; iColor++)
        {
            psGifCT->Colors[iColor].Red = 0;
            psGifCT->Colors[iColor].Green = 0;
            psGifCT->Colors[iColor].Blue = 0;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Setup parameters.                                               */
    /* -------------------------------------------------------------------- */
    if (EGifPutScreenDesc(hGifFile, nXSize, nYSize, 8, /* ColorRes */
                          255,                         /* Background */
                          psGifCT) == GIF_ERROR)
    {
        GifFreeMapObject(psGifCT);
        GDALPrintGifError(hGifFile, "Error writing gif file.");
        GIFAbstractDataset::myEGifCloseFile(hGifFile);
        VSIFCloseL(fp);
        return nullptr;
    }

    GifFreeMapObject(psGifCT);
    psGifCT = nullptr;

    // Support for transparency.
    int bNoDataValue = 0;
    double noDataValue = poBand->GetNoDataValue(&bNoDataValue);
    if (bNoDataValue && noDataValue >= 0 && noDataValue <= 255)
    {
        unsigned char extensionData[4] = {
            1,  // Transparent Color Flag.
            0, 0, static_cast<unsigned char>(noDataValue)};
        EGifPutExtension(hGifFile, 0xf9, 4, extensionData);
    }

    if (EGifPutImageDesc(hGifFile, 0, 0, nXSize, nYSize, bInterlace, nullptr) ==
        GIF_ERROR)
    {
        GDALPrintGifError(hGifFile, "Error writing gif file.");
        GIFAbstractDataset::myEGifCloseFile(hGifFile);
        VSIFCloseL(fp);
        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Loop over image, copying image data.                            */
    /* -------------------------------------------------------------------- */
    GDALPamDataset *poDS = nullptr;
    GByte *pabyScanline = static_cast<GByte *>(CPLMalloc(nXSize));

    if (!pfnProgress(0.0, nullptr, pProgressData))
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Unable to setup progress.");
    }

    if (!bInterlace)
    {
        for (int iLine = 0; iLine < nYSize; iLine++)
        {
            const CPLErr eErr = poBand->RasterIO(
                GF_Read, 0, iLine, nXSize, 1, pabyScanline, nXSize, 1, GDT_Byte,
                nBands, static_cast<GSpacing>(nBands) * nXSize, nullptr);

            if (eErr != CE_None ||
                EGifPutLine(hGifFile, pabyScanline, nXSize) == GIF_ERROR)
            {
                CPLError(CE_Failure, CPLE_AppDefined,
                         "Error writing gif file.");
                goto error;
            }

            if (!pfnProgress((iLine + 1) * 1.0 / nYSize, nullptr,
                             pProgressData))
            {
                goto error;
            }
        }
    }
    else
    {
        int nLinesRead = 0;
        // Need to perform 4 passes on the images:
        for (int i = 0; i < 4; i++)
        {
            for (int j = InterlacedOffset[i]; j < nYSize;
                 j += InterlacedJumps[i])
            {
                const CPLErr eErr =
                    poBand->RasterIO(GF_Read, 0, j, nXSize, 1, pabyScanline,
                                     nXSize, 1, GDT_Byte, 1, nXSize, nullptr);

                if (eErr != CE_None ||
                    EGifPutLine(hGifFile, pabyScanline, nXSize) == GIF_ERROR)
                {
                    CPLError(CE_Failure, CPLE_AppDefined,
                             "Error writing gif file.");
                    goto error;
                }

                nLinesRead++;
                if (!pfnProgress(nLinesRead * 1.0 / nYSize, nullptr,
                                 pProgressData))
                {
                    goto error;
                }
            }
        }
    }

    CPLFree(pabyScanline);
    pabyScanline = nullptr;

    /* -------------------------------------------------------------------- */
    /*      cleanup                                                         */
    /* -------------------------------------------------------------------- */
    if (GIFAbstractDataset::myEGifCloseFile(hGifFile) == GIF_ERROR)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "EGifCloseFile() failed.");
        hGifFile = nullptr;
        goto error;
    }
    hGifFile = nullptr;

    VSIFCloseL(fp);
    fp = nullptr;

    /* -------------------------------------------------------------------- */
    /*      Do we need a world file?                                        */
    /* -------------------------------------------------------------------- */
    if (CPLFetchBool(papszOptions, "WORLDFILE", false))
    {
        double adfGeoTransform[6] = {};

        if (poSrcDS->GetGeoTransform(adfGeoTransform) == CE_None)
            GDALWriteWorldFile(pszFilename, "wld", adfGeoTransform);
    }

    /* -------------------------------------------------------------------- */
    /*      Re-open dataset, and copy any auxiliary pam information.         */
    /* -------------------------------------------------------------------- */

    // If writing to stdout, we can't reopen it, so return
    // a fake dataset to make the caller happy.
    CPLPushErrorHandler(CPLQuietErrorHandler);
    poDS = static_cast<GDALPamDataset *>(GDALOpen(pszFilename, GA_ReadOnly));
    CPLPopErrorHandler();
    if (poDS)
    {
        poDS->CloneInfo(poSrcDS, GCIF_PAM_DEFAULT);
        return poDS;
    }
    else
    {
        CPLErrorReset();

        GIFDataset *poGIF_DS = new GIFDataset();
        poGIF_DS->nRasterXSize = nXSize;
        poGIF_DS->nRasterYSize = nYSize;
        for (int i = 0; i < nBands; i++)
            poGIF_DS->SetBand(i + 1,
                              new GIFRasterBand(poGIF_DS, i + 1, nullptr, 0));
        return poGIF_DS;
    }

error:
    if (hGifFile)
        GIFAbstractDataset::myEGifCloseFile(hGifFile);
    if (fp)
        VSIFCloseL(fp);
    if (pabyScanline)
        CPLFree(pabyScanline);
    return nullptr;
}

/************************************************************************/
/*                          GDALRegister_GIF()                          */
/************************************************************************/

void GDALRegister_GIF()

{
    if (GDALGetDriverByName(GIF_DRIVER_NAME) != nullptr)
        return;

    GDALDriver *poDriver = new GDALDriver();

    GIFDriverSetCommonMetadata(poDriver);
    poDriver->pfnOpen = GIFDataset::Open;
    poDriver->pfnCreateCopy = GIFDataset::CreateCopy;

    GetGDALDriverManager()->RegisterDriver(poDriver);

#ifdef GIF_PLUGIN
    GDALRegister_BIGGIF();
#endif
}
